﻿using System;
using System.Net;
using System.Net.Sockets;

namespace PacketRush.Core
{
    /// <summary>
    /// The default client-side networking provider.
    /// </summary>
    /// <remarks>
    /// <c>DefaultClientNetworkingProvider</c> provides client-side networking using TCP for connection and reliable packets and
    /// UDP for unreliable packets.
    /// </remarks>
    public class DefaultClientNetworkingProvider : IClientNetworkingProvider
    {
        /// <summary>
        /// The TCP end point of server
        /// </summary>
        private EndPoint _RemoteEndPoint;

        /// <summary>
        /// The IASync result associated to the current BeginConnect/EndConnect Async operation
        /// </summary>
        private IAsyncResult _ConnectResult;

        private Socket _UdpSocket;

        private DefaultConnection _Connection;

        /// <summary>
        /// Used as a temporary buffer for Unreliable receives
        /// </summary>
        private byte[] _ReceiveBuffer = new byte[Packet.MaxPacketSize];

        /// <summary>
        /// Connect to a server.
        /// </summary>
        /// <param name="remoteEndPoint">The remote end point of the server to connect to.</param>
        public void Connect(EndPoint remoteEndPoint)
        {
            try
            {
                _RemoteEndPoint = remoteEndPoint;

                Socket tcpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                _ConnectResult = tcpSocket.BeginConnect(_RemoteEndPoint, OnConnect, tcpSocket);
            }
            catch (SocketException x)
            {
                ErrorAction(new NetErrorEventArgs(null, x.SocketErrorCode));
            }
        }

        /// <summary>
        /// Occurs when a connection request is completed
        /// </summary>
        /// <param name="result"></param>
        private void OnConnect(IAsyncResult result)
        {
            try
            {
                Socket tcpSocket = (Socket)result.AsyncState;
                tcpSocket.EndConnect(result);

                CreateUdpSocket(tcpSocket.LocalEndPoint, tcpSocket.RemoteEndPoint);

                _Connection = new DefaultConnection(tcpSocket, ReceivePacketAction);
            }
            catch (SocketException x)
            {
                ErrorAction(new NetErrorEventArgs(null, x.SocketErrorCode));
            }
        }

        private void CreateUdpSocket(EndPoint localEndPoint, EndPoint remoteEndPoint)
        {
            _UdpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            _UdpSocket.Bind(localEndPoint);
            _UdpSocket.Connect(remoteEndPoint);
        }

        /// <summary>
        /// Accept the connection.
        /// </summary>
        /// <returns>Returns the connection.</returns>
        public INetConnection AcceptConnection()
        {
            if (_ConnectResult != null && _ConnectResult.IsCompleted &&
                _Connection != null)
                return _Connection;

            return null;
        }

        /// <summary>
        /// Raised when this provider encounters an error.
        /// </summary>
        public Action<NetErrorEventArgs> ErrorAction { get; set; }

        /// <summary>
        /// Send a packet to the given connection reliably.
        /// </summary>
        /// <param name="target">The target connection to send to.</param>
        /// <param name="packet">The raw packet to send.</param>
        public void SendReliable(INetConnection target, byte[] packet)
        {
            try
            {
                DefaultConnection conn = (DefaultConnection)target;
                conn.Send(packet);
            }
            catch (SocketException x)
            {
                ErrorAction(new NetErrorEventArgs(target, x.SocketErrorCode));
            }
        }

        /// <summary>
        /// Send a packet to the given connection unreliably.
        /// </summary>
        /// <param name="target">The target connection to send to.</param>
        /// <param name="packet">The raw packet to send.</param>
        public void SendUnreliable(INetConnection target, byte[] packet)
        {
            try
            {
                _UdpSocket.SendTo(packet, target.RemoteEndPoint);
            }
            catch (SocketException x)
            {
                ErrorAction(new NetErrorEventArgs(target, x.SocketErrorCode));
            }
        }

        /// <summary>
        /// Receives network packets.
        /// </summary>
        public void Receive()
        {
            ReceiveReliable();
            ReceiveUnreliable();
        }

        /// <summary>
        /// Disconnect this provider from the network.
        /// </summary>
        public void Disconnect()
        {
            try
            {
                if (_Connection != null)
                {
                    _Connection.Disconnect();
                    _Connection = null;
                }

                _ConnectResult = null;

                if (_UdpSocket != null)
                {
                    _UdpSocket.Close();
                    _UdpSocket = null;
                }
            }
            catch (SocketException x)
            {
                ErrorAction(new NetErrorEventArgs(_Connection, x.SocketErrorCode));
            }
        }

        private void ReceiveReliable()
        {
            try
            {
                _Connection.Receive();
            }
            catch (SocketException x)
            {
                ErrorAction(new NetErrorEventArgs(_Connection, x.SocketErrorCode));
            }
        }
        private void ReceiveUnreliable()
        {
            EndPoint endPoint = null;
            try
            {
                if (_UdpSocket.Available > 0)
                {
                    endPoint = new IPEndPoint(0, 0);

                    int datagramSize = _UdpSocket.ReceiveFrom(_ReceiveBuffer, ref endPoint);

                    if (_Connection.RemoteEndPoint.Equals(endPoint))
                    {
                        // Copy this packet into a suitably sized one
                        byte[] rawPacket = new byte[datagramSize];
                        Buffer.BlockCopy(_ReceiveBuffer, 0, rawPacket, 0, datagramSize);

                        ReceivePacketAction(rawPacket, _Connection);
                    }
                }
            }
            catch (SocketException x)
            {
                ErrorAction(new NetErrorEventArgs(_Connection, x.SocketErrorCode));
            }
        }

        /// <summary>
        /// Performs additional networking logic.
        /// </summary>
        public void Update()
        {
            try
            {
                // Send pending packets
                _Connection.SendPending();
            }
            catch (SocketException x)
            {
                ErrorAction(new NetErrorEventArgs(_Connection, x.SocketErrorCode));
            }
        }

        /// <summary>
        /// Called when the provider receives packets during a <see cref="PacketRush.Core.DefaultClientNetworkingProvider.Receive"/>.
        /// </summary>
        public Action<byte[], INetConnection> ReceivePacketAction { get; set; }

        /// <summary>
        /// Time measured from when <see cref="PacketRush.Core.DefaultClientNetworkingProvider.Connect"/> was called.
        /// </summary>
        /// <remarks>
        /// Time measured from when <see cref="PacketRush.Core.DefaultClientNetworkingProvider.Connect"/> was called, resets to 
        /// zero on disconnection.
        /// </remarks>
        public NetTime RunningTime { get; set; }
    }
}
